What is Per-CPU Variables in the Linux Kernel: Lock-Free Programming Explained-Free Linux Device Driver Course Online

Per-CPU Variables in the Linux Kernel: Lock-Free Programming Explained
A free Linux kernel development course lecture — how the kernel scales to hundreds of cores without a lock in sight

Per-CPU variables are one of the Linux kernel’s core scalability tools, and they are the natural, more elegant answer to the false-sharing problem covered in the previous lecture of this free Linux kernel development course. Instead of protecting one shared variable with a lock, the kernel simply gives every CPU its own private copy of the data. No lock, no cacheline contention, no waiting. This lecture explains per-CPU variables from first principles, walks through the modern kernel API (updated for kernel 6.12 LTS through the current 7.1 stable series), and gives you a working kernel module example.

What You Will Learn
Why per-CPU data is lock-free DEFINE_PER_CPU and friends this_cpu_* operations get_cpu_var vs modern APIs percpu_counter PREEMPT_RT considerations
Prerequisites
Basic kernel module structure False sharing and cache coherency (previous lecture) What preemption means in the kernel Basic C pointers

Why Per-CPU Variables Exist

In the previous lecture of this free Linux device drivers course, you saw that when multiple cores write to one shared variable, cache coherency traffic can seriously hurt performance — even when there’s no logical data race. Padding fixes the layout problem, but it doesn’t remove the underlying issue: it’s still one logical counter that every core has to agree on.

Per-CPU variables take a different approach entirely. Instead of one shared counter, the kernel allocates one independent copy of the variable for every CPU on the system. A thread running on CPU 2 only ever touches CPU 2’s copy. There is no sharing at all, so there is nothing to invalidate, nothing to coherency-check across cores, and no lock required. This is roughly the kernel-space equivalent of thread-local storage (__thread / thread_local) in user-space C.

One Per-CPU Variable = One Private Copy per Core
CPU 0
rx_count = 4021
CPU 1
rx_count = 3987
CPU 2
rx_count = 4110
CPU 3
rx_count = 3995

Each core updates only its own copy of rx_count. Reading a global total means summing all copies, only when you actually need the aggregate.

Declaring and Using a Per-CPU Variable

The kernel provides macros to declare per-CPU data, both statically at compile time and dynamically at runtime. A statically declared counter looks like this:

#include <linux/percpu.h>

DEFINE_PER_CPU(unsigned long, rx_packet_count);

This single line allocates one unsigned long for every possible CPU on the system, laid out so that each CPU’s copy lives in memory local to that CPU where the platform supports it (NUMA-aware placement). To safely increment the current CPU’s copy, modern kernel code uses the this_cpu_* family of operations, which are guaranteed atomic with respect to the current CPU without needing to explicitly disable preemption yourself:

/* modern, preferred style (kernel 6.x and current) */
this_cpu_inc(rx_packet_count);

unsigned long total_here = this_cpu_read(rx_packet_count);

The Older get_cpu_var / put_cpu_var Pattern

Older kernel code (and a lot of textbook material) uses a different, more manual pattern:

get_cpu_var(rx_packet_count)++;
put_cpu_var(rx_packet_count);

get_cpu_var() disables kernel preemption, hands you a reference to the local CPU’s copy, and put_cpu_var() re-enables preemption once you’re done. This works, but it’s easy to get wrong — forgetting put_cpu_var(), or doing too much work (like taking a sleeping lock) between the two calls, is a real bug class. This is exactly why current kernel code favors the this_cpu_* operations instead: they fold the “disable preemption, touch it, re-enable” dance into a single call, and the kernel documentation now marks the manual get_cpu_var pattern as something to avoid in new code unless you specifically need the CPU pinned across several operations.

API Style Preemption Handling Recommended Today?
get_cpu_var() / put_cpu_var() Manual — you must bracket every access Legacy; avoid in new code
this_cpu_read/write/inc/add() Handled internally, single call Yes — current best practice
percpu_counter API Fully managed, with batched global sum Yes — for counters read globally and often

Dynamic Per-CPU Allocation

For per-CPU data that isn’t known at compile time (for instance, per-device statistics allocated when a driver probes hardware), the kernel provides dynamic allocation:

struct driver_stats __percpu *stats;

stats = alloc_percpu(struct driver_stats);
if (!stats)
    return -ENOMEM;

/* on the fast path, from any CPU: */
this_cpu_inc(stats->rx_packets);

/* on cleanup: */
free_percpu(stats);

The __percpu annotation is a sparse/compiler hint that documents “this pointer refers to per-CPU memory, don’t dereference it directly” — it helps catch a whole class of bugs at build time where someone accidentally treats per-CPU memory like a normal shared pointer.

A Minimal Working Kernel Module Example

Here is a small, original example module demonstrating a static per-CPU counter incremented from a simple kernel timer, and read back on module removal.

#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/percpu.h>
#include <linux/smp.h>

static DEFINE_PER_CPU(unsigned long, tick_count);

static void bump_counter(void *unused)
{
    this_cpu_inc(tick_count);
}

static int __init percpu_demo_init(void)
{
    /* run bump_counter once on every online CPU */
    on_each_cpu(bump_counter, NULL, 1);
    pr_info("percpu_demo: loaded, one tick recorded per CPU\n");
    return 0;
}

static void __exit percpu_demo_exit(void)
{
    int cpu;
    unsigned long total = 0;

    for_each_online_cpu(cpu)
        total += per_cpu(tick_count, cpu);

    pr_info("percpu_demo: total ticks across all CPUs = %lu\n", total);
}

module_init(percpu_demo_init);
module_exit(percpu_demo_exit);
MODULE_LICENSE("GPL");

Notice the pattern in percpu_demo_exit(): reading the aggregate total means explicitly looping over every online CPU with per_cpu(variable, cpu) and summing. There is no automatic “global value” — that is the whole point of per-CPU data, and it’s also the one place many beginners get confused.

The percpu_counter API: When You Need Fast Reads Too

Manually summing across every CPU on every read gets expensive if you need the aggregate frequently (for example, checking free disk blocks on every write). For that pattern, the kernel provides percpu_counter, which keeps a per-CPU delta plus an approximate global counter that’s synced in batches, giving you fast increments and reasonably fast reads:

struct percpu_counter blocks_free;

percpu_counter_init(&blocks_free, 1000, GFP_KERNEL);
percpu_counter_add(&blocks_free, -1);   /* fast, per-CPU */
s64 approx_free = percpu_counter_read(&blocks_free);        /* fast, approximate */
s64 exact_free  = percpu_counter_sum(&blocks_free);          /* slower, exact */

percpu_counter_destroy(&blocks_free);

This exact structure is used throughout the kernel for filesystem free-space accounting and network statistics, precisely because those counters are updated extremely often but only need to be read exactly on rare, non-critical paths.

Per-CPU Variables and PREEMPT_RT

Since the PREEMPT_RT real-time preemption patches were merged into mainline starting with kernel 6.12, it’s worth understanding how per-CPU data interacts with a fully preemptible kernel. On a PREEMPT_RT kernel, code that used to rely on implicitly disabling preemption (the older get_cpu_var() pattern) needs extra care, because even “atomic” sections can be preempted by higher-priority real-time tasks in ways that were not possible on a traditional kernel. The this_cpu_* operations remain correct on PREEMPT_RT because they don’t assume the CPU stays fixed across multiple steps — another reason the modern API is the safer default for any driver or module you write today, embedded or otherwise.

Common Mistakes and Troubleshooting

Mistake: Treating a Per-CPU Pointer Like a Normal Pointer

Dereferencing a __percpu pointer directly is undefined behavior. Always go through this_cpu_*, per_cpu(), or per_cpu_ptr() accessors.

Mistake: Using Per-CPU Data for Large Structures

Every per-CPU variable is duplicated once per possible CPU. On a system configured for a large number of CPUs, a large per-CPU structure wastes real memory. Keep per-CPU data small.

Mistake: Expecting an Automatic Global Total

A plain per-CPU counter has no built-in “sum across all cores” — you must loop and add manually, or use percpu_counter if you need frequent, cheap reads.

Best Practices

  • Prefer this_cpu_* operations over the older get_cpu_var/put_cpu_var pattern in new code.
  • Use dynamic alloc_percpu() for per-device or per-instance data; static DEFINE_PER_CPU() for truly global counters.
  • Use percpu_counter when you need both fast writes and reasonably fast aggregate reads.
  • Keep per-CPU structures small and cacheline-friendly — they are still subject to false sharing internally if you pack multiple hot sub-fields together.
  • Always pair alloc_percpu() with free_percpu() to avoid per-CPU memory leaks.

Performance and Security Considerations

Per-CPU variables scale close to linearly with core count for the write path, since there’s no coherency traffic at all — this is precisely why they are the kernel’s go-to tool for hot counters on systems with dozens or hundreds of cores. The trade-off is memory: every additional CPU means another copy. From a security and correctness standpoint, remember that per-CPU data protects against races between CPUs, not against races between a normal task and an interrupt handler running on the very same CPU; if your per-CPU variable is also touched from interrupt context on that CPU, you still need local_irq_save()/local_irq_restore() or the interrupt-safe variants of the this_cpu_* operations.

Key Takeaways
  • Per-CPU variables give every core its own private copy of hot data, eliminating cache coherency overhead entirely.
  • Modern kernel code favors this_cpu_* operations over the older get_cpu_var/put_cpu_var pattern.
  • Use percpu_counter when you need both cheap increments and reasonably fast global reads.
  • Per-CPU data protects across cores, not automatically across interrupt context on the same core.

Conclusion

Per-CPU variables are the cleanest, most scalable answer to the cache-contention problems introduced in the previous lecture of this free Linux kernel development course. Once you’re comfortable with DEFINE_PER_CPU(), this_cpu_* operations, and the percpu_counter API, you have the same tool the kernel itself relies on for scheduler statistics, networking counters, and memory-management bookkeeping across every architecture Linux supports. Practice by converting a small shared counter in your own kernel module into a per-CPU counter and measuring the difference with perf stat.

Frequently Asked Questions

Are per-CPU variables the same as thread-local storage?

They’re conceptually similar — both give each execution context a private copy of data — but per-CPU variables are indexed by CPU, not by thread. A thread that migrates between CPUs will see different per-CPU copies unless it’s pinned to one core.

Do I need a lock when using this_cpu_inc?

No. this_cpu_inc() is safe with respect to other CPUs by design. You only need extra protection (like disabling interrupts) if the same per-CPU variable is also touched from an interrupt handler on that same CPU.

How do I read the total value across all CPUs?

Loop with for_each_online_cpu() and sum using per_cpu(variable, cpu), or use the percpu_counter API if you need frequent, fast aggregate reads.

Can per-CPU variables be used in user-space programs?

Per-CPU variables as described here are a Linux kernel-space mechanism. In user space, the closest equivalent is thread-local storage (__thread or C11 thread_local), though it’s indexed by thread rather than by CPU.

Why does the kernel prefer this_cpu_* over get_cpu_var today?

this_cpu_* operations are less error-prone (no matching put call to forget), and they remain correct on a fully preemptible PREEMPT_RT kernel, where the older manual pattern needs extra care.

Is per-CPU memory wasted if a CPU is offline?

The kernel allocates space for possible CPUs, and modern kernels are efficient about this, but very large per-CPU structures on systems configured for a high CPU count can still add up. Keep per-CPU data compact.

Continue This Free Linux Kernel Development Course

This lecture is part of EmbeddedPathashala’s free Linux kernel development course, free Linux device drivers course, and free embedded systems course.

Explore All Free Courses

2 Comments

Leave a Reply

Your email address will not be published. Required fields are marked *